Repository navigation
[XNNPACK] Lower upsample_nearest2d x2 as concatenate4 + depth_to_space_2d - #24113
Conversation
…ow-rank broadcast operands Profiling a conv encoder-decoder (MI-GAN inpainting, fp16) showed the graph split into 7-8 XNNPACK partitions around portable upsample_nearest2d ops and ~25% of the time spent in NCHW<->NHWC transposes. upsample_nearest2d.vec with an exact x2 scale on a static 4D input is now partitioned and lowered as concatenate4 of the input along channels followed by depth_to_space_2d(block_size=2). XNNPACK has no nearest resize and its static_broadcast is not implemented at run time. This adds an XNNDepthToSpace2d node to the flatbuffer schemas and XNNCompiler. Static quantization is supported: the quantizer annotates the op with shared input/output qparams and the intermediate value carries them. Other scales, including an explicit scale_factors that is not exactly 2.0, stay portable. In ChannelsLastTaggedReshapePass, single-element operands of add/sub/mul/div/maximum/minimum (a lifted Python scalar such as the gain in x * 1.4142) no longer take part in the layout decision, and rank < 4 operands that broadcast over the trailing dims (a [H, W] map added to [N, C, H, W]) are reshaped to 4D with leading ones so they follow the NHWC operand instead of dragging the activation back to NCHW. Dequantized operands keep their rank, since a view between the dequantize and the op would carry no quant params.
🔗 Helpful Links🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/24113
Note: Links to docs will display an error until the docs builds have been completed. ❗ 1 Active SEVsThere are 1 currently active SEVs. If your PR is affected, please view them below: ❌ 2 New FailuresAs of commit b2eb31c with merge base 422cc39 ( NEW FAILURES - The following jobs have failed:
This comment was automatically generated by Dr. CI and updates every 15 minutes. |
|
@pytorchbot label "release notes: xnnpack" |
|
sorry, can we split this into two PRs? to keep the changes atomic per pr instead of squishing them together. the upsample changes are additive and lower risk. that being said, a lower-rank operand with symbolic dims breaks lowering with these changes. Before, a dynamic-shape model with |
I will look into that tomorrow |
|
Done, split in two: this PR now only lowers Thanks for the dynamic-shape report, reproduced it: |
|
a few things:
|
…lowered case depth_to_space_2d requires its output to carry the input's quantization parameters. When the output is quantized differently, the visitor now writes depth_to_space into an intermediate with the input's parameters and requantizes it with a convert, so such a graph loads and runs instead of failing at load. The x2 constraint is shared between the partitioner and the quantizer through why_not_x2_nearest_upsample, which also reads scale_factors from kwargs. The annotator skips every upsample_nearest2d the partitioner would not lower, so a non-x2 upsample stays a float op without q/dq around it. The qs8 test asserted on a quantized_decomposed name that does not exist in the edge graph; it now checks quantization on the exported graph and relies on the single-partition check after lowering.
|
Thanks, all four addressed in b2eb31c.
|
### Summary Found while profiling the fp16 MI-GAN inpainting model. A zero `constant_pad_nd` before a convolution costs a full extra pass over the activation, which is noticeably slow in NHWC. In MI-GAN these pads, one before every 4x4 blur filter, took 13-22% of the forward. `FusePadIntoConvPass` folds a zero-valued `constant_pad_nd` on the two spatial dims whose only user is a non-transposed convolution into that convolution's padding. XNNPACK takes top/right/bottom/left padding independently, so asymmetric pads such as `F.pad(x, (2, 1, 2, 1))` before a 4x4 filter fold too. The graph and its shape metadata stay as they are: the pass only tags nodes. The pad visitor aliases the pad output to its input, and the conv visitor serializes the combined padding. The pass runs last, after `ChannelsLastTaggedReshapePass` and `RemoveRedundantCopyPass`. If a layout copy sits between the pad and the conv, the conv is no longer the pad's only user and nothing folds. Quantized pads are left alone. `InsertPadQDQPass` runs earlier and puts q/dq around pads fed by a dequantize, and the fold skips any pad whose input is a dequantize or whose user is a quantize. A pad in a quantized context therefore still becomes a quantized static pad, as before. Non-zero pad values, transposed convolutions and pads with more than one user are also not folded. No runtime change is needed: TRBL conv padding already exists in the runtime. The fp32 <-> fp16 cast lowering that was originally part of this PR now lives in #24277, as asked in review. Results measured on an ExecuTorch 1.4.1-based branch, with the pad fold and the cast lowering from #24277 together. They include the `upsample_nearest2d` lowering from #24113, without which a layout copy sits between every MI-GAN pad and its conv and nothing folds. Timings are `Method::execute` for MI-GAN 256 / 512 / faces_256 fp16, 10 timed runs after 2 warm-ups, output bit-identical: | | 256 | 512 | faces_256 | | --- | --- | --- | --- | | Mac M4 Pro, 14 threads | 34 -> 27 ms (-21%) | 58 -> 41 ms (-30%) | 33 -> 27 ms (-17%) | | Galaxy S23 (SD 8 Gen 2), 4 threads | 108 -> 105 ms (-3%) | 189 -> 182 ms (-3%) | 109 -> 107 ms (-2%) | The host gain is larger because XNNPACK's `Constant Pad` is unusually slow on the M4 Pro. On the device, the casts alone are within noise, so nearly all of the difference is the fold. Review order: `fuse_pad_into_conv_pass.py`, then `op_static_constant_pad.py` and `op_conv2d.py`, then the tests. ### Test plan `backends/xnnpack/test/passes/test_fuse_pad_into_conv_pass.py`: conv-pad-conv in fp32 and fp16, with a depthwise conv, with a width-only two-element pad, with a dilated conv and with dynamic height and width. Each asserts through the pass stage that the pad is tagged as folded and that the conv has the expected combined TRBL padding (existing conv padding included), then lowers, runs and compares outputs. Pads that must stay unfused (non-zero value, transposed conv, pad with two users, pad on the graph input, qs8 graph with `InsertPadQDQPass` in the pipeline) are checked the same way and must stay correct. ```bash python -m pytest backends/xnnpack/test/passes/test_fuse_pad_into_conv_pass.py # 11 passed python -m pytest backends/xnnpack/test/passes/test_fuse_pad_into_conv_pass.py \ backends/xnnpack/test/ops/test_conv2d.py \ backends/xnnpack/test/ops/test_static_constant_pad.py \ backends/xnnpack/test/passes/test_insert_pad_qdq.py # 58 passed ``` Run on macOS (Apple Silicon, Python 3.13) against the prebuilt pybindings. `lintrunner` on the changed files reports no issues. cc @nil-is-all @GregoryComer @digantdesai @cbilgin @JakeStevens
Summary
Profiling the MI-GAN inpainting model (a conv encoder-decoder, fp16) on XNNPACK showed the graph split into 7-8 delegate partitions around portable
upsample_nearest2dops. This PR lowersupsample_nearest2dx2.XNNPACK has no nearest resize, and its
static_broadcastis defined but returnsxnn_status_unsupported_parameterat reshape time. Nearest x2 is instead lowered asconcatenate4of the input with itself along channels ([N, H, W, 4C]), followed bydepth_to_space_2d(block_size=2). This adds anXNNDepthToSpace2dnode, appended at the end of theXNodeUnioninschema.fbs/runtime_schema.fbsso existing node IDs don't change, plusdefineDepthToSpace2dNodeinXNNCompiler.cpp.UpsampleNearest2dConfigonly partitions static 4D inputs with an exact x2 output. An explicitscale_factorsthat is not exactly 2.0 stays portable as well: PyTorch indexes with the given scale rather than the size ratio, soscale_factor=2.004doubles the size but picks different pixels. Static quantization is supported: the quantizer annotates the op with input/output shared qparams, and the intermediate value carries them, since neither node changes values. When a graph quantizes the output differently from the input,depth_to_space_2dwrites into an intermediate with the input's parameters and anXNNConvertrequantizes it, because XNNPACK requires both to match. The x2 constraint lives inwhy_not_x2_nearest_upsample, shared byUpsampleNearest2dConfigand the annotator, so the quantizer only annotates what the partitioner lowers.The
ChannelsLastTaggedReshapePasschange that was originally part of this PR (keeping NHWC across scalar and low-rank broadcast operands) moved to its own PR at review request, together with the fix for the dynamic-shape low-rank operand case reported below.Measured on MI-GAN 256/512 fp16 with the host runtime, together with that pass change, on an ExecuTorch 1.4.1-based branch: XNNPACK partitions went from 7/8 to 1, the delegated share of run time from 87/82% to 98/95%, and
Method::executedropped by 29%/28%. The output was unchanged (max abs diff 2e-3, fp16 noise).The new node needs an updated runtime. A program that uses it fails to load on an older runtime.
Review order: the serialization schema and
XNNCompiler.cpp, thenop_upsample_nearest2d.pywithUpsampleNearest2dConfigand the quantizer annotator, then the tests.Test plan
Tested on macOS arm64, with pybindings built from this branch (
cmake --preset pybind, targetportable_lib) so that the run-on-delegate stages use the new runtime node.backends/xnnpack/test/ops/test_upsample_nearest2d.pycovers x2 by scale and by size, fp16, fp32 and qs8 conv-upsample-conv lowered to one partition, a qs8 output quantized differently from the input, x3 andscale_factor=2.004staying portable and not annotated, andscale_factorspassed as a kwarg.python -m pytest backends/xnnpack/test/ops/test_upsample_nearest2d.py backends/xnnpack/test/passes/test_channels_last_tagged_reshape.py backends/xnnpack/test/test_xnnpack_partitioner.py # 38 passedlintrunnerreports no issues on the changed files.